#!/usr/bin/python

# Copyright (c) 2017, Kairo Araujo <kairo@kairo.eti.br>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations


DOCUMENTATION = r"""
author:
  - Kairo Araujo (@kairoaraujo)
module: aix_filesystem
short_description: Configure LVM and NFS file systems for AIX
description:
  - This module creates, removes, mount and unmount LVM and NFS file system for AIX using C(/etc/filesystems).
  - For LVM file systems is possible to resize a file system.
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  account_subsystem:
    description:
      - Specifies whether the file system is to be processed by the accounting subsystem.
    type: bool
    default: false
  attributes:
    description:
      - Specifies attributes for files system separated by comma.
    type: list
    elements: str
    default:
      - agblksize=4096
      - isnapshot=no
  auto_mount:
    description:
      - File system is automatically mounted at system restart.
    type: bool
    default: true
  device:
    description:
      - Logical volume (LV) device name or remote export device to create a NFS file system.
      - It is used to create a file system on an already existing logical volume or the exported NFS file system.
      - If not mentioned a new logical volume name is created following AIX standards (LVM).
    type: str
  fs_type:
    description:
      - Specifies the virtual file system type.
    type: str
    default: jfs2
  permissions:
    description:
      - Set file system permissions. V(rw) (read-write) or V(ro) (read-only).
    type: str
    choices: [ro, rw]
    default: rw
  mount_group:
    description:
      - Specifies the mount group.
    type: str
  filesystem:
    description:
      - Specifies the mount point, which is the directory where the file system will be mounted.
    type: str
    required: true
  nfs_server:
    description:
      - Specifies a Network File System (NFS) server.
    type: str
  rm_mount_point:
    description:
      - Removes the mount point directory when used with state V(absent).
    type: bool
    default: false
  size:
    description:
      - Specifies the file system size.
      - For already present it resizes the filesystem.
      - 512-byte blocks, megabytes or gigabytes. If the value has M specified it is in megabytes. If the value has G specified
        it is in gigabytes.
      - If no M or G the value is 512-byte blocks.
      - If V(+) is specified in begin of value, the value is added.
      - If V(-) is specified in begin of value, the value is removed.
      - If neither V(+) nor V(-) is specified, then the total value is the specified.
      - Size respects the LVM AIX standards.
    type: str
  state:
    description:
      - Controls the file system state.
      - V(present) check if file system exists, creates or resize.
      - V(absent) removes existing file system if already V(unmounted).
      - V(mounted) checks if the file system is mounted or mount the file system.
      - V(unmounted) check if the file system is unmounted or unmount the file system.
    type: str
    choices: [absent, mounted, present, unmounted]
    default: present
  vg:
    description:
      - Specifies an existing volume group (VG).
    type: str
notes:
  - For more O(attributes), please check "crfs" AIX manual.
"""

EXAMPLES = r"""
- name: Create filesystem in a previously defined logical volume.
  community.general.aix_filesystem:
    device: testlv
    filesystem: /testfs
    state: present

- name: Creating NFS filesystem from nfshost.
  community.general.aix_filesystem:
    device: /home/ftp
    nfs_server: nfshost
    filesystem: /home/ftp
    state: present

- name: Creating a new file system without a previously logical volume.
  community.general.aix_filesystem:
    filesystem: /newfs
    size: 1G
    state: present
    vg: datavg

- name: Unmounting /testfs.
  community.general.aix_filesystem:
    filesystem: /testfs
    state: unmounted

- name: Resizing /mksysb to +512M.
  community.general.aix_filesystem:
    filesystem: /mksysb
    size: +512M
    state: present

- name: Resizing /mksysb to 11G.
  community.general.aix_filesystem:
    filesystem: /mksysb
    size: 11G
    state: present

- name: Resizing /mksysb to -2G.
  community.general.aix_filesystem:
    filesystem: /mksysb
    size: -2G
    state: present

- name: Remove NFS filesystem /home/ftp.
  community.general.aix_filesystem:
    filesystem: /home/ftp
    rm_mount_point: true
    state: absent

- name: Remove /newfs.
  community.general.aix_filesystem:
    filesystem: /newfs
    rm_mount_point: true
    state: absent
"""


from ansible.module_utils.basic import AnsibleModule
from os.path import ismount
import re


def _fs_exists(module, filesystem):
    """
    Check if file system already exists on /etc/filesystems.

    :param module: Ansible module.
    :param community.general.filesystem: filesystem name.
    :return: True or False.
    """
    lsfs_cmd = module.get_bin_path("lsfs", True)
    rc, lsfs_out, err = module.run_command([lsfs_cmd, "-l", filesystem])
    if rc == 1:
        if re.findall("No record matching", err):
            return False

        else:
            module.fail_json(msg=f"Failed to run lsfs. Error message: {err}")

    else:
        return True


def _check_nfs_device(module, nfs_host, device):
    """
    Validate if NFS server is exporting the device (remote export).

    :param module: Ansible module.
    :param nfs_host: nfs_host parameter, NFS server.
    :param device: device parameter, remote export.
    :return: True or False.
    """
    showmount_cmd = module.get_bin_path("showmount", True)
    rc, showmount_out, err = module.run_command([showmount_cmd, "-a", nfs_host])
    if rc != 0:
        module.fail_json(msg=f"Failed to run showmount. Error message: {err}")
    else:
        showmount_data = showmount_out.splitlines()
        return any(line.split(":")[1] == device for line in showmount_data)


def _validate_vg(module, vg):
    """
    Check the current state of volume group.

    :param module: Ansible module argument spec.
    :param vg: Volume Group name.
    :return: True (VG in varyon state) or False (VG in varyoff state) or
             None (VG does not exist), message.
    """
    lsvg_cmd = module.get_bin_path("lsvg", True)
    rc, current_active_vgs, err = module.run_command([lsvg_cmd, "-o"])
    if rc != 0:
        module.fail_json(msg=f"Failed executing {lsvg_cmd} command.")

    rc, current_all_vgs, err = module.run_command([lsvg_cmd])
    if rc != 0:
        module.fail_json(msg=f"Failed executing {lsvg_cmd} command.")

    if vg in current_all_vgs and vg not in current_active_vgs:
        msg = f"Volume group {vg} is in varyoff state."
        return False, msg
    elif vg in current_active_vgs:
        msg = f"Volume group {vg} is in varyon state."
        return True, msg
    else:
        msg = f"Volume group {vg} does not exist."
        return None, msg


def resize_fs(module, filesystem, size):
    """Resize LVM file system."""

    chfs_cmd = module.get_bin_path("chfs", True)
    if not module.check_mode:
        rc, chfs_out, err = module.run_command([chfs_cmd, "-a", f"size={size}", filesystem])

        if rc == 28:
            changed = False
            return changed, chfs_out
        elif rc != 0:
            if re.findall("Maximum allocation for logical", err):
                changed = False
                return changed, err
            else:
                module.fail_json(msg=f"Failed to run chfs. Error message: {err}")

        else:
            if re.findall("The filesystem size is already", chfs_out):
                changed = False
            else:
                changed = True

            return changed, chfs_out
    else:
        changed = True
        msg = ""

        return changed, msg


def create_fs(
    module,
    fs_type,
    filesystem,
    vg,
    device,
    size,
    mount_group,
    auto_mount,
    account_subsystem,
    permissions,
    nfs_server,
    attributes,
):
    """Create LVM file system or NFS remote mount point."""

    attributes = " -a ".join(attributes)

    # Parameters definition.
    account_subsys_opt = {True: "-t yes", False: "-t no"}

    if nfs_server is not None:
        auto_mount_opt = {True: "-A", False: "-a"}

    else:
        auto_mount_opt = {True: "-A yes", False: "-A no"}

    if size is None:
        size = ""
    else:
        size = f"-a size={size}"

    if device is None:
        device = ""
    else:
        device = f"-d {device}"

    if vg is None:
        vg = ""
    else:
        vg_state, msg = _validate_vg(module, vg)
        if vg_state:
            vg = f"-g {vg}"
        else:
            changed = False

            return changed, msg

    if mount_group is None:
        mount_group = ""

    else:
        mount_group = f"-u {mount_group}"

    auto_mount = auto_mount_opt[auto_mount]
    account_subsystem = account_subsys_opt[account_subsystem]

    if nfs_server is not None:
        # Creates a NFS file system.
        mknfsmnt_cmd = module.get_bin_path("mknfsmnt", True)
        if not module.check_mode:
            rc, mknfsmnt_out, err = module.run_command(
                [mknfsmnt_cmd, "-f", filesystem, device, "-h", nfs_server, "-t", permissions, auto_mount, "-w", "bg"]
            )
            if rc != 0:
                module.fail_json(msg=f"Failed to run mknfsmnt. Error message: {err}")
            else:
                changed = True
                msg = f"NFS file system {filesystem} created."

                return changed, msg
        else:
            changed = True
            msg = ""

            return changed, msg

    else:
        # Creates a LVM file system.
        crfs_cmd = module.get_bin_path("crfs", True)
        if not module.check_mode:
            cmd = [crfs_cmd]

            cmd.append("-v")
            cmd.append(fs_type)

            if vg:
                (flag, value) = vg.split()
                cmd.append(flag)
                cmd.append(value)

            if device:
                (flag, value) = device.split()
                cmd.append(flag)
                cmd.append(value)

            cmd.append("-m")
            cmd.append(filesystem)

            if mount_group:
                (flag, value) = mount_group.split()
                cmd.append(flag)
                cmd.append(value)

            if auto_mount:
                (flag, value) = auto_mount.split()
                cmd.append(flag)
                cmd.append(value)

            if account_subsystem:
                (flag, value) = account_subsystem.split()
                cmd.append(flag)
                cmd.append(value)

            cmd.append("-p")
            cmd.append(permissions)

            if size:
                (flag, value) = size.split()
                cmd.append(flag)
                cmd.append(value)

            if attributes:
                splitted_attributes = attributes.split()
                cmd.append("-a")
                for value in splitted_attributes:
                    cmd.append(value)

            rc, crfs_out, err = module.run_command(cmd)

            if rc == 10:
                module.exit_json(
                    msg=f"Using a existent previously defined logical volume, volume group needs to be empty. {err}"
                )

            elif rc != 0:
                module.fail_json(msg=f"Failed to run {cmd}. Error message: {err}")

            else:
                changed = True
                return changed, crfs_out
        else:
            changed = True
            msg = ""

            return changed, msg


def remove_fs(module, filesystem, rm_mount_point):
    """Remove an LVM file system or NFS entry."""

    # Command parameters.
    rm_mount_point_opt = {True: "-r", False: ""}

    rm_mount_point = rm_mount_point_opt[rm_mount_point]

    rmfs_cmd = module.get_bin_path("rmfs", True)
    if not module.check_mode:
        cmd = [rmfs_cmd, "-r", rm_mount_point, filesystem]
        rc, rmfs_out, err = module.run_command(cmd)
        if rc != 0:
            module.fail_json(msg=f"Failed to run {cmd}. Error message: {err}")
        else:
            changed = True
            msg = rmfs_out
            if not rmfs_out:
                msg = f"File system {filesystem} removed."

            return changed, msg
    else:
        changed = True
        msg = ""

        return changed, msg


def mount_fs(module, filesystem):
    """Mount a file system."""
    mount_cmd = module.get_bin_path("mount", True)

    if not module.check_mode:
        rc, mount_out, err = module.run_command([mount_cmd, filesystem])
        if rc != 0:
            module.fail_json(msg=f"Failed to run mount. Error message: {err}")
        else:
            changed = True
            msg = f"File system {filesystem} mounted."

            return changed, msg
    else:
        changed = True
        msg = ""

        return changed, msg


def unmount_fs(module, filesystem):
    """Unmount a file system."""
    unmount_cmd = module.get_bin_path("unmount", True)

    if not module.check_mode:
        rc, unmount_out, err = module.run_command([unmount_cmd, filesystem])
        if rc != 0:
            module.fail_json(msg=f"Failed to run unmount. Error message: {err}")
        else:
            changed = True
            msg = f"File system {filesystem} unmounted."

            return changed, msg
    else:
        changed = True
        msg = ""

        return changed, msg


def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_subsystem=dict(type="bool", default=False),
            attributes=dict(type="list", elements="str", default=["agblksize=4096", "isnapshot=no"]),
            auto_mount=dict(type="bool", default=True),
            device=dict(type="str"),
            filesystem=dict(type="str", required=True),
            fs_type=dict(type="str", default="jfs2"),
            permissions=dict(type="str", default="rw", choices=["rw", "ro"]),
            mount_group=dict(type="str"),
            nfs_server=dict(type="str"),
            rm_mount_point=dict(type="bool", default=False),
            size=dict(type="str"),
            state=dict(type="str", default="present", choices=["absent", "mounted", "present", "unmounted"]),
            vg=dict(type="str"),
        ),
        supports_check_mode=True,
    )

    account_subsystem = module.params["account_subsystem"]
    attributes = module.params["attributes"]
    auto_mount = module.params["auto_mount"]
    device = module.params["device"]
    fs_type = module.params["fs_type"]
    permissions = module.params["permissions"]
    mount_group = module.params["mount_group"]
    filesystem = module.params["filesystem"]
    nfs_server = module.params["nfs_server"]
    rm_mount_point = module.params["rm_mount_point"]
    size = module.params["size"]
    state = module.params["state"]
    vg = module.params["vg"]

    result = dict(
        changed=False,
        msg="",
    )

    if state == "present":
        fs_mounted = ismount(filesystem)
        fs_exists = _fs_exists(module, filesystem)

        # Check if fs is mounted or exists.
        if fs_mounted or fs_exists:
            result["msg"] = f"File system {filesystem} already exists."
            result["changed"] = False

            # If parameter size was passed, resize fs.
            if size is not None:
                result["changed"], result["msg"] = resize_fs(module, filesystem, size)

        # If fs doesn't exist, create it.
        else:
            # Check if fs will be a NFS device.
            if nfs_server is not None:
                if device is None:
                    result["msg"] = 'Parameter "device" is required when "nfs_server" is defined.'
                    module.fail_json(**result)
                else:
                    # Create a fs from NFS export.
                    if _check_nfs_device(module, nfs_server, device):
                        result["changed"], result["msg"] = create_fs(
                            module,
                            fs_type,
                            filesystem,
                            vg,
                            device,
                            size,
                            mount_group,
                            auto_mount,
                            account_subsystem,
                            permissions,
                            nfs_server,
                            attributes,
                        )

            if device is None:
                if vg is None:
                    result["msg"] = 'Required parameter "device" and/or "vg" is missing for filesystem creation.'
                    module.fail_json(**result)
                else:
                    # Create a fs from
                    result["changed"], result["msg"] = create_fs(
                        module,
                        fs_type,
                        filesystem,
                        vg,
                        device,
                        size,
                        mount_group,
                        auto_mount,
                        account_subsystem,
                        permissions,
                        nfs_server,
                        attributes,
                    )

            if device is not None and nfs_server is None:
                # Create a fs from a previously lv device.
                result["changed"], result["msg"] = create_fs(
                    module,
                    fs_type,
                    filesystem,
                    vg,
                    device,
                    size,
                    mount_group,
                    auto_mount,
                    account_subsystem,
                    permissions,
                    nfs_server,
                    attributes,
                )

    elif state == "absent":
        if ismount(filesystem):
            result["msg"] = f"File system {filesystem} mounted."

        else:
            fs_status = _fs_exists(module, filesystem)
            if not fs_status:
                result["msg"] = f"File system {filesystem} does not exist."
            else:
                result["changed"], result["msg"] = remove_fs(module, filesystem, rm_mount_point)

    elif state == "mounted":
        if ismount(filesystem):
            result["changed"] = False
            result["msg"] = f"File system {filesystem} already mounted."
        else:
            result["changed"], result["msg"] = mount_fs(module, filesystem)

    elif state == "unmounted":
        if not ismount(filesystem):
            result["changed"] = False
            result["msg"] = f"File system {filesystem} already unmounted."
        else:
            result["changed"], result["msg"] = unmount_fs(module, filesystem)

    else:
        # Unreachable codeblock
        result["msg"] = f"Unexpected state {state}."
        module.fail_json(**result)

    module.exit_json(**result)


if __name__ == "__main__":
    main()
